package kode.boot.file.web;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import kode.base.core.AppResult;
import kode.base.core.NotExistException;
import kode.base.core.util.ArrayUtil;
import kode.base.core.util.ConvertUtil;
import kode.base.core.util.FileUtil;
import kode.base.core.util.StringUtil;
import kode.base.web.util.WebUtil;
import kode.boot.file.api.FileMetadata;
import kode.boot.file.api.FileMetadataService;
import kode.boot.file.api.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.activation.MimetypesFileTypeMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * FileController，只保存文件，不保存 metadata
 *
 * @author Stark
 * @date 2019/1/5
 */
@RestController
@Api("文件上传下载")
@RequestMapping("${mapping.file:/file}")
public class FileController<T extends FileMetadata<ID>, ID> {

	private final static String ATTACHMENT_FILENAME = "attachment; filename=";

	@Autowired
	private FileMetadataService<T, ID> metadataService;
	@Autowired
	private FileService<T, ID> fileService;

	/**
	 * 根据文件名和内容输出文本文件
	 *
	 * @param name 文件名，带后缀
	 * @param text 文本内容
	 * @throws IOException i/o error occurs
	 */
	@ApiOperation(value = "构建文本文件并下载", httpMethod = "GET, POST")
	@GetMapping("/text/{name}")
	@PostMapping("/text/{name}")
	public void downloadText(
			@ApiParam(value = "文件名", required = true)
			@PathVariable("name")
					String name,
			@ApiParam(value = "文本内容, get 或者 form post", required = true)
			@RequestParam(value = "text")
					String text,
			@ApiParam(hidden = true)
					HttpServletResponse response
	) throws IOException {
		response.setCharacterEncoding("UTF-8");
		response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=utf-8");
		response.setHeader(WebUtil.CONTENT_DISPOSITION, String.format(WebUtil.ATTACHMENT_FILENAME_FORMAT, URLEncoder.encode(name, StandardCharsets.UTF_8)));
		response.getWriter().write(text);
	}

	@PostMapping("/zip/{name}")
	public void downloadZip(
			@ApiParam(value = "下载时的文件名", required = true)
			@PathVariable("name")
					String name,
			@ApiParam(value = "下载 zip 文件中包含的文件信息", required = true, example = "[" +
					"{\"name\":\"readme.md\",\"text\":\"test md\"}," +
					"{\"name\":\"logo.png\",\"file\":\"id or location\"}" +
					"{\"name\":\"site.ico\",\"base64\":\"base64chars=\"}" +
					"]")
			@RequestParam("data")
					String data,
			@ApiParam(hidden = true)
					HttpServletResponse response
	) throws IOException {
		//TODO: download zip
		List<Map<String, String>> fileList = new ObjectMapper().readValue(data, new TypeReference<List<Map<String, String>>>() {
		});

		response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
		response.setHeader(WebUtil.CONTENT_DISPOSITION, String.format(WebUtil.ATTACHMENT_FILENAME_FORMAT, URLEncoder.encode(name, StandardCharsets.UTF_8)));

//		fileList.forEach(map -> {
//			String entryName = map.get("name");
//			String text = map.get("text");
//			String file = map.get("file");
//			String fileBase64 = map.get("base64");
//			if (text != null) {
//				writer.addTextFile(map.get("name"), text);
//			} else if (file != null) {
//				writer.addFile(entryName, fileService.readFile(file));
//			} else if (fileBase64 != null) {
//				writer.addFile(entryName, new ByteArrayInputStream(Base64.decodeBase64(fileBase64)));
//			}
//		});
//		writer.write(response.getOutputStream());
	}

	@ApiOperation("通过文件元数据编号下载文件，支持断点续传")
	@GetMapping("/id/{id}")
	public void downloadById(
			@ApiParam(value = "文件元信息编号", required = true)
			@PathVariable
					ID id,
			@ApiParam("下载文件时指定文件名，不包含文件扩展名")
			@RequestParam(value = "name", required = false)
					String name,
			@ApiParam("断点续传位置，只支持单段续传")
			@RequestHeader(value = "Range", required = false)
					String range,
			@ApiParam(hidden = true)
					HttpServletResponse response
	) throws IOException {

		T meta = metadataService.getById(id).orElse(null);
		if (meta == null) {
			throw new NotExistException("文件记录不存在");
		}

		// 决定最终下载时候显示的文件名，有传入文件名用传入文件名，没有用保存的文件名
		String fileName = StringUtil.isEmpty(name) ? meta.getName() : name;
		String contentType = meta.getContentType() != null
				? meta.getContentType()
				: MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(meta.getName());
		long size = fileService.getFileSize(meta.getLocation());
		writeToStream(meta.getLocation(), fileName, size, contentType, range, response);
	}

	@ApiOperation("通过文件路径下载文件，支持断线续传")
	@GetMapping("/p/**")
	public void downloadByPath(
			@ApiParam("下载文件时指定文件名，不包含文件扩展名")
			@RequestParam(value = "name", required = false)
					String name,
			@ApiParam("断点续传位置，只支持单段续传")
			@RequestHeader(value = "Range", required = false)
					String range,
			@ApiParam(hidden = true) HttpServletRequest request,
			@ApiParam(hidden = true) HttpServletResponse response
	) throws IOException {

		// file path extracted does not contain '/'
		String fileLocation = "/" + WebUtil.extractPathWithinPattern(request);
		String fileName = StringUtil.isEmpty(name) ? FileUtil.getFileName(fileLocation) : name;
		String contentType = MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(fileLocation);
		long size = fileService.getFileSize(fileLocation);

		writeToStream(fileLocation, fileName, size, contentType, range, response);
	}

	private void writeToStream(String location, String fileName,
	                           long size,
	                           String contentType, String range,
	                           HttpServletResponse response) throws IOException {
		long skip = 0;
		// 断点续传支持
		if (!StringUtil.isEmpty(range)) {
			String rangeStr = range.replace("bytes=", "");
			String startStr = rangeStr.substring(0, rangeStr.indexOf("-"));
			skip = ConvertUtil.toLong(startStr, 0);
		}

		// 响应头设置及输出文件
		response.setContentLengthLong(size);
		response.setContentType(contentType);
		response.setHeader(WebUtil.CONTENT_DISPOSITION, String.format(WebUtil.ATTACHMENT_FILENAME_FORMAT, URLEncoder.encode(fileName, StandardCharsets.UTF_8)));
		if (skip > 0) {
			String contentRange = "bytes " + skip + "-" + (size - 1) + "/" + size;
			response.setHeader("Content-Range", contentRange);
			response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
		}

		fileService.writeToStream(location, response.getOutputStream(), skip);
	}

	@ApiOperation("上传文件，同时支持单文件和多文件上传，如果两个字段同时有值，多值字段将会忽略。")
	@PostMapping("/upload")
	public AppResult<?> upload(
			@ApiParam("单文件上传字段域")
			@RequestPart(value = "file", required = false)
					MultipartFile file,
			@ApiParam("多文件上传字段域")
			@RequestPart(value = "files", required = false)
					MultipartFile[] files
	) throws IOException {

		if (file != null && !file.isEmpty()) {
			// upload single file

			T meta = uploadSingleFile(file, true);
			return AppResult.success(meta);
		}

		if (ArrayUtil.isNotEmpty(files) && Arrays.stream(files).anyMatch(c -> !c.isEmpty())) {
			// upload one by one
			List<T> list = new ArrayList<>();
			for (MultipartFile f : files) {
				if (!f.isEmpty()) {
					T t = uploadSingleFile(f, true);
					list.add(t);
				}
			}

			return AppResult.success(list);
		}

		return AppResult.failure("上传文件为空");
	}

	@ApiOperation("上传文件，不保元数据信息，")
	@PostMapping("/upload/static")
	public AppResult<?> uploadStatic(
			@RequestPart(value = "file", required = false) MultipartFile file,
			@RequestPart(value = "files", required = false) MultipartFile[] files
	) throws IOException {
		if (file != null && !file.isEmpty()) {
			// upload single file

			T meta = uploadSingleFile(file, false);
			return AppResult.success(meta.getLocation());
		}

		if (ArrayUtil.isNotEmpty(files) && Arrays.stream(files).anyMatch(c -> !c.isEmpty())) {
			// upload one by one
			List<String> list = new ArrayList<>();
			for (MultipartFile f : files) {
				if (!f.isEmpty()) {
					T t = uploadSingleFile(f, false);
					list.add(t.getLocation());
				}
			}

			return AppResult.success(list);
		}

		return AppResult.failure("上传文件为空");
	}

	private T uploadSingleFile(MultipartFile file, boolean saveMetadata) throws IOException {
		T metadata = metadataService.newInstance();
		FileMetadata.load(metadata, file);

		if (saveMetadata) {
			fileService.saveFileAndMeta(file.getInputStream(), metadata);
		} else {
			fileService.saveFile(file.getInputStream(), metadata);
		}

		return metadata;
	}
}

